SPDX-FileCopyrightText: 2025 Jakub Niemynski & Carla Treinen SPDX-FileCopyrightText: 2025 AlICe laboratory https://alicelab.be
SPDX-License-Identifier: GPL-3.0-or-later
import bpy
import math
import random
import bmeshClear all objects and orphan data
bpy.ops.object.select_all(action="SELECT")
bpy.ops.object.delete(use_global=False)
bpy.ops.outliner.orphans_purge()List (grid) that represents the coordinates in an x by y dimension
def grid(x_size, y_size, empty=False):
    grid = []
    for i in range(x_size):
        row = []
        for j in range(y_size):
            if not empty:
                x = i
                y = j
                row.append((x, y))
            else:
                row.append(None)
        grid.append(row)
    return gridStepcount which whill be used later on
def shift(pattern, step=1):
    part_one = pattern[step:]
    part_two = pattern[:step]
    new_pattern = part_one + part_two
    return new_patternCheck if two points are direct neighbors or diagonal neighbors
def is_direct_neighbour(p1, p2):
    x1, y1 = p1
    x2, y2 = p2
    return (x1 == x2 and abs(y1 - y2) == 1) or (  # Horizontal neighbor
        y1 == y2 and abs(x1 - x2) == 1
    )  # Vertical neighbordef is_diagonal_neighbour(p1, p2):
    x1, y1 = p1
    x2, y2 = p2
    return abs(x1 - x2) == 1 and abs(y1 - y2) == 1  # Diagonal neighborFunction to create a cylinder at a given location
def create_cylinder(location, radius, height, name):
    bpy.ops.mesh.primitive_cylinder_add(radius=radius, depth=height, location=location)
    cylinder = bpy.context.object
    cylinder.name = nameFunction to create a mesh from vertices and faces
def create_mesh(name, vertices, faces):
    mesh = bpy.data.meshes.new(name)
    mesh.from_pydata(vertices, [], faces)
    mesh.update()
    obj = bpy.data.objects.new(name, mesh)
    bpy.context.collection.objects.link(obj)Function to create grid
def create_grid(axis, base_location, dimensions, num_copies, spacing):Creates a grid of cubes along a specified axis.
:param axis: The axis along which to create the grid (‘x’ or ‘y’). :param base_location: Tuple (x, y, z) for the starting location of the grid. :param dimensions: Tuple (width, depth, height) for the cube’s dimensions. :param num_copies: Number of cubes to create in the grid. :param spacing: Space between cubes.
    width, depth, height = dimensionsCreate the initial cube
    bpy.ops.mesh.primitive_cube_add(size=1, location=base_location)
    cube = bpy.context.object
    cube.scale = (width, depth, height)
    cube.name = f"CustomCube_{axis}_Grid"Ensure the mesh is unique
    cube.data = cube.data.copy()Duplicate cubes along the specified axis
    for i in range(1, num_copies):
        new_cube = cube.copy()
        new_cube.data = new_cube.data.copy()  # Ensure no shared mesh data
        if axis == "x":
            new_cube.location.x = base_location[0] + i * (width + spacing)
        elif axis == "y":
            new_cube.location.y = base_location[1] + i * (depth + spacing)
        bpy.context.collection.objects.link(new_cube)Define parameters for the grids
grid_params = [
    ("x", (0, 6, -3), (0.1, 12, 0.1), 11, 0.9),  # Grid along X-axis (lower level)
    ("y", (6, 0, -3), (12, 0.1, 0.1), 11, 0.9),  # Grid along Y-axis (lower level)
    ("x", (0, 6, 3), (0.1, 12, 0.1), 11, 0.9),  # Grid along X-axis (upper level)
    ("y", (6, 0, 3), (12, 0.1, 0.1), 11, 0.9),  # Grid along Y-axis (upper level)
]Create the grids
for params in grid_params:
    create_grid(*params)
print("Grids created successfully!")Initial grid dimensions
size = 12Generate coordinates for grids
grid_down = grid(size, size)
grid_up = grid(size, size)
empty_down = grid(size, size, empty=False)
empty_up = grid(size, size, empty=False)Define voice patterns
voice_down = []
while True not in voice_down:
    voice_down = [random.choice([True, False]) for _ in range(size)]
voice_up = voice_downFill grid_down without shifts
for row in grid_down:
    for coord, is_voice in zip(row, voice_down):
        if not is_voice:
            r = grid_down.index(row)
            i = row.index(coord)
            grid_down[r][i] = None
            empty_down[r][i] = coord
print("Initial Grid Down:\t", grid_down)Fill grid_up with consistent shifts
shift_amount = random.randint(1, size)
current_voice_up = shift(voice_up, shift_amount)
for row in grid_up:
    for coord, is_voice in zip(row, current_voice_up):
        if not is_voice:
            r = grid_up.index(row)
            i = row.index(coord)
            grid_up[r][i] = None
            empty_up[r][i] = coord
    current_voice_up = shift(current_voice_up, shift_amount)
print("Initial Grid UP:\t", grid_up)Create double_columns and clean grid_up/grid_down
double_columns = []
new_grid_up = []
new_grid_down = []
for row_up, row_down in zip(grid_up, grid_down):
    for up_coord, down_coord in zip(row_up, row_down):
        if up_coord and down_coord:
            double_columns.append(up_coord)
            r = grid_up.index(row_up)
            i = row_up.index(up_coord)
            grid_up[r][i] = None
            grid_down[r][i] = None
print("Double Columns:\t\t", double_columns)
print("New Grid Down:\t\t", grid_down)
print("New Grid UP:\t\t", grid_up)Populate new_grid_up and new_grid_down
for row in grid_up:
    for coord in row:
        if coord:
            new_grid_up.append(coord)
for row in grid_down:
    for coord in row:
        if coord:
            new_grid_down.append(coord)Parameters for cylinders
height = 3.5
radius_GridDown = 0.36
radius_GridUp = 0.18
z_GridDown = 1.25
z_GridUp = -1.25Create cylinders for double_columns
for coord in double_columns:
    x, y = coord
    create_cylinder(
        location=(x, y, z_GridDown),
        radius=radius_GridDown,
        height=height,
        name=f"Cylinder_GridDown_{coord}",
    )
    create_cylinder(
        location=(x, y, z_GridUp),
        radius=radius_GridUp,
        height=height,
        name=f"Cylinder_GridUp_{coord}",
    )Identify neighboring pairs (direct and diagonal neighbors)
neighbour_pairs = []
ortho = []
for coord1 in new_grid_up + new_grid_down:
    for coord2 in new_grid_up + new_grid_down:
        if is_direct_neighbour(coord1, coord2):
            neighbour_pairs.append((coord1, coord2))
            ortho.append(coord1)
            ortho.append(coord2)
        elif (coord1 not in ortho and coord2 not in ortho) and is_diagonal_neighbour(
            coord1, coord2
        ):
            neighbour_pairs.append((coord1, coord2))
print("Neighbor Pairs (Direct and Diagonal):")
for pair in neighbour_pairs:
    print(pair)Create meshes for neighboring pairs (direct and diagonal neighbors)
for coord1, coord2 in neighbour_pairs:
    x1, y1 = coord1
    x2, y2 = coord2Check grid membership and determine z-values
    if coord1 in new_grid_down:
        z1_low, z1_high = -3, 0
    elif coord1 in new_grid_up:
        z1_low, z1_high = 0, 3
    else:
        continue
    if coord2 in new_grid_down:
        z2_low, z2_high = -3, 0
    elif coord2 in new_grid_up:
        z2_low, z2_high = 0, 3
    else:
        continueDefine vertices
    vertices = [
        (x1, y1, z1_low),
        (x1, y1, z1_high),
        (x2, y2, z2_low),
        (x2, y2, z2_high),
    ]Define faces
    faces = [(0, 1, 3, 2)]Create mesh
    mesh_name = f"Mesh_{coord1}_{coord2}"
    create_mesh(mesh_name, vertices, faces)
print("Cylinders and meshes for direct and diagonal neighbors created successfully!")Solidify meshes
for obj in bpy.data.objects:
    if "Mesh_" in obj.name:add solidify
        mod2 = bpy.data.objects[obj.name].modifiers.new(
            name="solidify", type="SOLIDIFY"
        )
        mod2.thickness = 0.15Random Cube Placement
random_range = 12  # Range for random placement of x and y
random_x = random.uniform(3, random_range - 3)
random_y = random.uniform(3, random_range - 3)
z_location = 0  # Fixed Z location for the cubeCube dimensions
cube_size_x = 6
cube_size_y = 6
cube_size_z = 7Create the cube = subtractive object
bpy.ops.mesh.primitive_cube_add(size=1, location=(random_x, random_y, z_location))
cube = bpy.context.object
cube.scale = (cube_size_x, cube_size_y, cube_size_z)
cube.name = "BooleanCube"Apply Boolean Modifier to All Mesh Objects
for obj in bpy.context.scene.objects:
    if obj == cube or obj.type != "MESH":
        continueAdd the boolean modifier
    boolean_modifier = obj.modifiers.new(name="BooleanModifier", type="BOOLEAN")
    boolean_modifier.object = cube
    boolean_modifier.operation = "INTERSECT"Apply the modifier
    bpy.context.view_layer.objects.active = obj
    try:
        bpy.ops.object.modifier_apply(modifier=boolean_modifier.name)
        print(f"Boolean applied to {obj.name}.")
    except Exception as e:
        print(f"Failed to apply boolean on {obj.name}: {e}")Remove the cube after all operations are complete
bpy.data.objects.remove(cube, do_unlink=True)
print("Cube removed.")
print("Final cube successfully created.")